Skip to content

feat(transaction-pay-controller): add generic non-atomic post-Relay flow via atomic and recipient - #9497

Merged
OGPoyraz merged 17 commits into
mainfrom
ogp/enable-max-withdraw-to-ma
Jul 31, 2026
Merged

feat(transaction-pay-controller): add generic non-atomic post-Relay flow via atomic and recipient#9497
OGPoyraz merged 17 commits into
mainfrom
ogp/enable-max-withdraw-to-ma

Conversation

@OGPoyraz

@OGPoyraz OGPoyraz commented Jul 14, 2026

Copy link
Copy Markdown
Member

Explanation

Introduces a generic non-atomic post-Relay flow on transaction-pay-controller, driven by a new field on TransactionConfig / TransactionData:

  • atomic?: boolean — when false, TPC skips atomic quote embedding (no processTransactions, no processMoneyAccountPostQuote). The Relay quote bridges/swaps only, then a second leg runs after completion.

This unblocks two previously-blocked flows without adding another bespoke MA-specific module:

  • Perps/Predict withdraws to Money Account — post-quote flows whose destination is mUSD on Monad. Settled amount is only known after Relay completes.
  • Max-amount Money Account depositsisMaxAmount forces EXACT_INPUT, so vault calls cannot be pre-encoded at quote time.

For both, Relay settles mUSD directly to a recipient derived internally at quote time (no client-configurable recipient field); once complete, a sponsored addTransactionBatch runs the second leg (approve + teller deposit for post-quote flows; the transaction's own nested calls for non-post-quote flows).

Recipient derivation (quote time)

resolveNonAtomicRecipient in relay-quotes.ts derives where the Relay output must settle for non-atomic flows:

  • Post-quote (Perps/Predict withdraw → MA): calls the client getPaymentOverrideData callback — the same source of truth the atomic path already uses via processMoneyAccountPostQuote — and uses its returned recipient (the Money Account address, which cannot be derived from the request). Falls back to from if the callback returns no recipient.
  • Non-post-quote (max-amount MA deposit): uses the parent transaction's own txParams.from, which is the Money Account rather than the accountOverride-resolved funding EOA. Without this, max deposits settled mUSD on the EOA where the sponsored vault batch and CHOMP backup could not reach it.

The derived recipient is persisted on quote.request.recipient (internal-only) so the post-completion leg reads/settles against the correct account.

Changes

types.ts — Adds atomic to TransactionConfig and TransactionData. QuoteRequest.recipient is now internal-only (derived, not client-set).

TransactionPayController.ts — Propagates atomic through transactionDataconfig synchronisation (via spread + Object.assign).

utils/quotes.ts — Threads atomic through buildQuoteRequests / buildPostQuoteRequests.

strategy/relay/relay-quotes.ts

  • New resolveNonAtomicRecipient (see above); the derived recipient feeds the quote body and the normalized quote request.
  • processTransactions now returns a boolean and encapsulates all its skip conditions (skipProcessTransactions/isPostQuote, Polymarket deposit wallet, non-atomic) in a single early-return, so getSingleQuote routes to the alternate handlers when it declines.

strategy/relay/relay-submit.ts — After successful Relay completion, when atomic === false, runs submitPostNonAtomic:

  • Resolves the actually-settled amount via resolveSettledAmount:
    • Cross-chain: reads the Transfer log on the polled target hash; throws on read failure rather than guessing (using the minimum would knowingly strand dust and defeat EXACT_INPUT).
    • Same-chain: polling is skipped (placeholder target hash), so the hash returned by submitTransactions — the transaction that actually moved the funds — is read instead. Relay execute submissions return the FALLBACK_HASH placeholder rather than a real hash, so only in that case does the quote's minimum output remain as the last available source.
  • For isPostQuote === true, calls getPaymentOverrideData with the settled amount to build the deposit batch; prefers the callback's returned recipient as source of truth for the second-leg account. Throws Missing post-quote deposit calls if the callback returns empty — post-quote parent metas have no vault-side nested calls, so falling through to getAmountData would strand funds on the recipient.
  • For isPostQuote === false, falls through to the transaction's own nested calls re-encoded via getAmountData.
  • Delegates to submitMoneyAccountVaultDeposit.

utils/ma-vault-deposit.ts — Extends submitMoneyAccountVaultDeposit with two new optional params:

  • moneyAccountAddress — explicit MA address override (used when txParams.from isn't the MA, e.g. Perps/Predict withdraws).
  • depositCalls — pre-built batch. When provided, resolveVaultDepositBatch returns it directly, skipping the getAmountData re-encoding path.

Removedstrategy/relay/relay-post-ma-vault.ts and its tests. The dedicated MA post-Relay module is replaced by the generic submitPostNonAtomic in relay-submit.ts.

Same-chain payment-override prepend gate

Also fixes a pre-existing bug in relay-submit.ts: the paymentOverride prepend onto the source execute batch is skipped for non-atomic flows — their second leg is submitted separately after completion, so prepending would double-embed the vault deposit. Cross-chain atomic flows (e.g. moneyAccountWithdraw on Monad settling USDC on Arbitrum for Perps) still prepend the source-side vault withdraw + transfer so Relay has funds to bridge.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

High Risk
Changes Relay quote construction, post-completion settlement, and Money Account vault submission paths where incorrect amounts or recipients could strand user funds.

Overview
Adds atomic?: boolean on TransactionConfig / TransactionData / QuoteRequest (default true). When atomic is false, Relay quotes only bridge to an internally derived recipient—no embedded txs—and the second leg (vault deposit) runs after Relay succeeds via new submitPostNonAtomic in relay-submit.ts.

Quote time: resolveNonAtomicRecipient sets the bridge recipient (post-quote: getPaymentOverrideData; max-deposit: parent tx from). processTransactions returns a boolean and skips embedding for non-atomic flows.

Submit time: Settled amount comes from on-chain Transfer logs when possible (stricter than the removed module); post-quote legs use getPaymentOverrideData with the settled human amount and optional depositCalls / moneyAccountAddress on submitMoneyAccountVaultDeposit. Non-atomic flows skip paymentOverride prepend to avoid double-embedding the vault step.

Removed: relay-post-ma-vault.ts and its tests—replaced by the generic path above.

Reviewed by Cursor Bugbot for commit bbc766e. Bugbot is set up for automated code reviews on this repo. Configure here.

@OGPoyraz OGPoyraz changed the title Ogp/enable max withdraw to ma feat(transaction-pay-controller): enable max-amount withdraw to Money Account vault via post-Relay deposit Jul 14, 2026
@OGPoyraz
OGPoyraz marked this pull request as ready for review July 14, 2026 10:50
@OGPoyraz
OGPoyraz requested review from a team as code owners July 14, 2026 10:50
@OGPoyraz
OGPoyraz temporarily deployed to default-branch July 14, 2026 10:50 — with GitHub Actions Inactive
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-post-ma-vault.ts Outdated
@OGPoyraz
OGPoyraz force-pushed the ogp/enable-max-withdraw-to-ma branch from 8e85d4e to b13f8fe Compare July 14, 2026 11:17
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-post-ma-vault.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-post-ma-vault.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts Outdated
Comment thread packages/transaction-pay-controller/src/TransactionPayController.ts Outdated
Comment thread packages/transaction-pay-controller/src/TransactionPayController.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-post-ma-vault.ts Outdated
Comment thread packages/transaction-pay-controller/src/utils/transaction.ts Outdated
OGPoyraz added 2 commits July 21, 2026 16:24
Cross-chain flows (e.g. Predict withdraw on Polygon depositing to a Money
Account on Monad) carry the deposit in the Relay quote's destination txs[]
with a delegation signed for the destination chain. Prepending it onto the
source-chain execute batch made that delegation get redeemed on the source
chain, so the on-chain signature check recovered a wrong signer and reverted
with InvalidEOASignature() (0x3db6791c). Only prepend the override for
same-chain flows; cross-chain flows fall through to prepend the original tx.
@OGPoyraz
OGPoyraz force-pushed the ogp/enable-max-withdraw-to-ma branch from 23233f7 to acd0987 Compare July 22, 2026 07:50
@OGPoyraz OGPoyraz changed the title feat(transaction-pay-controller): enable max-amount withdraw to Money Account vault via post-Relay deposit feat(transaction-pay-controller): add generic non-atomic post-Relay flow via atomic and recipient Jul 22, 2026
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
@OGPoyraz
OGPoyraz requested a review from matthewwalsh0 July 22, 2026 08:53
Comment thread packages/transaction-pay-controller/src/TransactionPayController.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts Outdated
Comment thread packages/transaction-pay-controller/src/utils/quotes.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
@OGPoyraz
OGPoyraz requested a review from matthewwalsh0 July 27, 2026 11:39
@matthewwalsh0
matthewwalsh0 requested review from matthewwalsh0 and removed request for matthewwalsh0 July 27, 2026 23:08
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated
Comment thread packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cf0519b. Configure here.

@OGPoyraz
OGPoyraz requested a review from matthewwalsh0 July 31, 2026 06:39
@OGPoyraz
OGPoyraz added this pull request to the merge queue Jul 31, 2026
Merged via the queue into main with commit 0774efc Jul 31, 2026
46 checks passed
@OGPoyraz
OGPoyraz deleted the ogp/enable-max-withdraw-to-ma branch July 31, 2026 09:34
@OGPoyraz OGPoyraz mentioned this pull request Jul 31, 2026
4 tasks
pull Bot pushed a commit to Reality2byte/metamask-mobile that referenced this pull request Aug 4, 2026
…y deposit (MetaMask#33489)

<!--
Please submit this PR as a draft initially.

Do not mark it as "Ready for review" until this PR meets the canonical
Definition of Ready For Review in `docs/readme/ready-for-review.md`.

In short: the template must be materially complete (not just section
titles
present), all status checks must be currently passing, and the only
expected
follow-up commits must be reviewer-driven.
-->
<!--
mms-check directive vocabulary — read by
.github/scripts/shared/pr-template-checks.ts
at module load to build the validation plan. Directives are invisible in
rendered
markdown and must NOT be removed or edited without updating the
validator registry.

  type=text           Section must contain non-placeholder prose.
  type=changelog      Section must have a valid CHANGELOG entry: line.
type=issue-link Section must have a Fixes:/Closes:/Refs: line with a
value.
type=manual-testing Section must have real testing steps or an explicit
N/A.
type=screenshot Section must have evidence (image/URL) or an explicit
N/A.
type=checklist Section must have all checkboxes consciously checked.
required=true|false Whether a missing/invalid section runs the validator
at all.
blocking=true|false Whether a failure of this check fails the CI
workflow.
Default: false — failures are shown as warnings in the sticky
                      comment but do not block the PR.

Sections without a directive are checked for structural presence only.
-->

## **Description**

<!-- mms-check: type=text required=true -->

Consumes the new `atomic` primitive from
`@metamask/transaction-pay-controller` (see
[core#9497](MetaMask/core#9497)) to enable two
previously-blocked Money Account flows:

- **Perps/Predict withdraw → Money Account** — post-quote flow whose
destination is mUSD on Monad. Settled amount is only known after Relay
completes; the transfer to MA now runs as a sponsored post-Relay batch.
All perps/predict withdraws to MA take this non-atomic path (a max flag
cannot be set for these flows, and EXACT_INPUT keeps fee semantics
consistent with standard withdraws).
- **Max-amount Money Account deposit** — `isMaxAmount` forces
EXACT_INPUT so the vault deposit cannot be pre-encoded at quote time.
The vault approve + deposit now runs post-Relay against the
actually-settled mUSD.

The `atomic: false` flag on the transaction config drives the non-atomic
path in TPC. There is no client-configurable `recipient`: TPC derives
where the Relay output settles at quote time — via this app's
`getPaymentOverrideData` callback (which returns the Money Account
address) for post-quote flows, or the transaction's own `from` for max
deposits. Regular (non-max) Money Account deposits and Money Account
withdraws keep the existing atomic path unchanged.

### Changes

- **`transaction-pay.ts` (`applyMoneyAccountOverride`)** — takes
`transactionMeta` and derives whether to set `atomic: false`
(Perps/Predict withdraw) or `refundTo` (Money Account deposit). Money
Account withdraw only gets `paymentOverride`.
- **`transaction-pay.ts` (`setMoneyAccountDepositMaxAtomic`)** — new
helper that flips `config.atomic` on the max toggle for Money Account
deposits.
- **`useTransactionCustomAmount.ts` (`setIsMax`)** — calls
`setMoneyAccountDepositMaxAtomic` when the transaction is a Money
Account deposit, so toggling max on/off correctly flips the non-atomic
path.
- **`useClearPaymentOverride.ts`** — clears `atomic` alongside
`paymentOverride` and `refundTo` when the user switches away from Money
Account pay-with.
- **`paymentoverride-callback.ts`** — forks on `transactionData.atomic`:
atomic path keeps the delegation-wrapped calls; non-atomic path returns
raw `[approve, deposit]` (post-quote deposit) or `[withdraw, transfer]`
(withdraw to MA) so TPC can submit the second leg as a sponsored batch
from the Money Account. The returned `recipient` (Money Account address)
is what TPC uses to settle the Relay quote at quote time.
- **`useInsufficientPredictBalanceAlert.ts`** — fixes the fee check for
predict withdraws: they are EXACT_INPUT (fees deducted from the receive
amount, never added on top), so the old `amount + fees > balance` check
wrongly blocked near-full-balance withdraws. Now mirrors perps: alert
only when fees consume the entire amount.
- **`useInsufficientPerpsBalanceAlert.ts`** — drops the atomic-era
`amount + fees > balance` check for MA-override withdraws; under the
non-atomic flow they are EXACT_INPUT like standard withdraws. The plain
`balance < amount` and `fees >= amount` checks apply to all perps
withdraws.
- **Patch files** — re-generated `@metamask/transaction-pay-controller`
and `@metamask/transaction-controller` patches to match the new core API
surface.

## **Changelog**

<!-- mms-check: type=changelog required=true blocking=true -->

CHANGELOG entry: Enabled Perps/Predict withdraws to Money Account and
max-amount Money Account deposits via the new post-Relay sponsored
deposit flow.

## **Related issues**

<!-- mms-check: type=issue-link required=true -->

Fixes:

## **Manual testing steps**

<!-- mms-check: type=manual-testing required=true -->

```gherkin
Feature: Perps/Predict withdraw to Money Account

  Scenario: user withdraws from Perps to Money Account
    Given user has a Perps position with USDC balance on HyperLiquid
    And user has a Money Account
    When user opens the Perps withdraw flow
    And user selects Money Account as the destination
    And user enters an amount (any amount, including full balance)
    Then a quote appears with the entered amount as source amount
    When user confirms the transaction
    Then Relay settles mUSD directly on the Money Account
    And a sponsored batch approves + deposits the settled mUSD into the vault

  Scenario: user max-deposits into Money Account
    Given user has mUSD on Monad
    And user has a Money Account
    When user opens the Money Account deposit flow
    And user taps "Max"
    Then a quote appears with the full balance as source amount
    When user confirms the transaction
    Then Relay settles mUSD to the Money Account address
    And a sponsored batch approves + deposits mUSD into the vault

  Scenario: regular (non-max) Money Account deposit continues to work
    Given user has mUSD on Monad
    And user has a Money Account
    When user opens the Money Account deposit flow
    And user types a specific (non-max) amount
    Then a quote appears with the typed target amount
    When user confirms the transaction
    Then the atomic embedded vault deposit runs as before

  Scenario: Money Account withdraw to Perps continues to work
    Given user has mUSD in a Money Account
    And user has a Perps account
    When user opens the Money Account withdraw flow
    And user selects Perps as the destination
    And user types an amount
    Then a quote appears
    When user confirms the transaction
    Then the vault withdraw + transfer + Relay bridge complete atomically
```

## **Screenshots/Recordings**

<!-- mms-check: type=screenshot required=true -->

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

N/A — flow was blocked (no quote available for max Perps/Predict
withdraw to MA and max MA deposit).

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

<!-- mms-check: type=checklist required=true -->

<!--
Every checklist item must be consciously assessed before marking this PR
as
"Ready for review". A checked box means you deliberately considered that
responsibility, not that you literally performed every action listed.

Unchecked boxes are ambiguous: they are not an implicit "N/A" and they
are not
a silent "skip". See `docs/readme/ready-for-review.md` for the full
checklist
semantics.
-->

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

#### Performance checks (if applicable)

- [ ] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).

## **Pre-merge reviewer checklist**

<!--
Reviewer checklist items follow the same semantics as the author
checklist: an
unchecked box is ambiguous, a checked box means the reviewer consciously
assessed that responsibility. See `docs/readme/ready-for-review.md`.
-->

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Changes payment override, atomic batching, and vault deposit/withdraw
submission paths for real money flows; incorrect atomic or fee logic
could block quotes or mis-settle funds.
> 
> **Overview**
> Wires **`atomic: false`** through Money Account transaction pay so
Perps/Predict withdraws to the Money Account and **max** Money Account
deposits can run vault steps as a **post-Relay** sponsored batch instead
of only inside the atomic bundle.
> 
> **`applyMoneyAccountOverride`** now takes **`transactionMeta`** and
sets **`atomic: false`** for Perps/Predict withdraw, **`refundTo`** for
deposit-direction flows, and nothing extra for Money Account withdraw.
**`setMoneyAccountDepositMaxAtomic`** toggles **`atomic`** when max is
selected on Money Account deposit; **`useTransactionCustomAmount`**
calls it from **`setIsMax`** and treats predict withdraw like perps for
max (**`isMaxAmount`** stays off). **`useClearPaymentOverride`** clears
**`atomic`** unless a max Money Account deposit still needs **`atomic:
false`**.
> 
> **`getPaymentOverrideData`** branches on **`atomic`**: non-atomic
paths return raw withdraw/transfer or approve/deposit calls without
delegation wrap.
> 
> **`hasMax`** is enabled on Money Account deposit and Predict withdraw
UIs. Insufficient-balance alerts for predict and perps withdraws now use
**`getTotalPayFeesUsd`** and only flag fees that **consume the entire**
entered amount (EXACT_INPUT), removing the old **`amount + fees >
balance`** logic for Money Account override perps withdraws.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8d1b512. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants